// Copyright (c) Appeon. All rights reserved.
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 
// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Threading;

namespace SnapObjects.Expressions
{
    internal class DynamicFactory
    {
        public static readonly DynamicFactory Instance = new DynamicFactory();

        private int _classCount = 0;
        private readonly ModuleBuilder _module;
        private readonly ReaderWriterLockSlim _rwLock;
        private readonly IDictionary<DynamicSignature, Type> _classList;

        private DynamicFactory()
        {
            var name = new AssemblyName("SnapObjects.Expressions.Dynamics^z");

            var assembly = AssemblyBuilder.DefineDynamicAssembly(name, AssemblyBuilderAccess.Run);

            _module = assembly.DefineDynamicModule("Module");

            _classList = new ConcurrentDictionary<DynamicSignature, Type>();

            _rwLock = new ReaderWriterLockSlim();
        }

        public Type GetDynamicClass(IEnumerable<DynamicProperty> properties)
        {
            try
            {
                _rwLock.EnterWriteLock();

                var signature = new DynamicSignature(properties);

                if (!_classList.TryGetValue(signature, out var type))
                {
                    type = this.CreateDynamicClass(properties);

                    _classList.Add(signature, type);
                }

                return type;
            }
            finally
            {
                _rwLock.ExitWriteLock();
            }
        }

        private Type CreateDynamicClass(IEnumerable<DynamicProperty> properties)
        {
            var typeName = "z`" + _classCount++;

            var tb = _module.DefineType(typeName, TypeAttributes.Class | TypeAttributes.Public);

            var fields = this.GenerateProperties(tb, properties.ToArray());

            var result = tb.CreateTypeInfo().AsType();

            return result;
        }

        private FieldInfo[] GenerateProperties(TypeBuilder tb, DynamicProperty[] properties)
        {
            FieldInfo[] fields = new FieldBuilder[properties.Length];

            for (var i = 0; i < properties.Length; i++)
            {
                var dp = properties[i];
                var fb = tb.DefineField("_" + dp.Name, dp.Type, FieldAttributes.Private);
                var pb = tb.DefineProperty(dp.Name, PropertyAttributes.HasDefault, dp.Type, null);

                var mbGet = tb.DefineMethod("get_" + dp.Name,
                    MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig,
                    dp.Type, Type.EmptyTypes);

                var genGet = mbGet.GetILGenerator();
                genGet.Emit(OpCodes.Ldarg_0);
                genGet.Emit(OpCodes.Ldfld, fb);
                genGet.Emit(OpCodes.Ret);

                var mbSet = tb.DefineMethod("set_" + dp.Name,
                    MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig,
                    null, new Type[] { dp.Type });

                var genSet = mbSet.GetILGenerator();
                genSet.Emit(OpCodes.Ldarg_0);
                genSet.Emit(OpCodes.Ldarg_1);
                genSet.Emit(OpCodes.Stfld, fb);
                genSet.Emit(OpCodes.Ret);

                pb.SetGetMethod(mbGet);
                pb.SetSetMethod(mbSet);

                fields[i] = fb;
            }

            return fields;
        }
    }
}
